package com.redijedi.tapestry.internal.tree;

import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.CLOSE_DIV;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.CLOSE_ITEM;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.CLOSE_LIST;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.DO_NOTHING;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.OPEN_DIV;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.OPEN_ITEM;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.OPEN_LIST;
import static com.redijedi.tapestry.internal.tree.TreeRendererCommand.RENDER_BODY;

import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import com.redijedi.tapestry.ITreeNodeModel;
import com.redijedi.tapestry.IVisitable;
import com.redijedi.tapestry.IVisitor;

/**
 * 
 * @author torr
 * 
 */
public class TreeNodeVisitor implements IVisitor {

	private enum ElementType {
		ROOT_FIRST_VISIT, ROOT, ROOT_LAST_VISIT, BRANCH_FIRST_VISIT, BRANCH, BRANCH_LAST_VISIT, LEAF;
	}

	private Map<IVisitable, Integer> _visited = new IdentityHashMap<IVisitable, Integer>();

	private Queue<TreeRendererCommandNodeSet> _commands = new LinkedList<TreeRendererCommandNodeSet>();

	public void visit(IVisitable visitable) {
		if (visitable instanceof ITreeNodeModel) {
			visit((ITreeNodeModel<?>) visitable);
		}
	}

	private void visit(ITreeNodeModel<?> node) {
		// avoid null nodes
		if (node == null) {
			return;
		}
		
		// this depends on the visit count so it must be run before incrementing
		ElementType type = determineElementType(node);

		// increment the number of visits to this object
		incrementVisitCount(node);

		// handle setup before going down into the tree
		preTraversal(node, type);

		// performs the visit on all the children
		// this goes down into the leaves
		for (IVisitable n : node.getChildren()) {
			// only descend into children once if they haven't been visited
			// yet
			if (visitCount(n) == 0) {
				visit(n);
			}
		}
		
		type = determineElementType(node);

		// handle backing out of the tree
		postTraversal(node, type);
	}

	public Queue<TreeRendererCommandNodeSet> getRendererCommands() {
		return _commands;
	}

	private ElementType determineElementType(ITreeNodeModel<?> node) {
		int count = visitCount(node);

		if (node.getParent() == null && count == 0) {
			return ElementType.ROOT_FIRST_VISIT;
		} else if (node.getParent() == null && allChildrenHaveBeenVisited(node)) {
			return ElementType.ROOT_LAST_VISIT;
		} else if (node.getParent() == null) {
			return ElementType.ROOT;
		} else if (node.getChildren().size() > 0 && count == 0) {
			return ElementType.BRANCH_FIRST_VISIT;
		} else if (node.getChildren().size() > 0
				&& allChildrenHaveBeenVisited(node)) {
			return ElementType.BRANCH_LAST_VISIT;
		} else if (node.getChildren().size() > 0) {
			return ElementType.BRANCH;
		} else {
			return ElementType.LEAF;
		}
	}

	private boolean allChildrenHaveBeenVisited(ITreeNodeModel<?> node) {
		boolean allChildrenVisited = true;
		for (IVisitable v : node.getChildren()) {
			if (visitCount(v) == 0) {
				allChildrenVisited = false;
				break;
			}
		}
		return allChildrenVisited;
	}

	private int visitCount(IVisitable node) {
		Integer visits = 0;
		if (_visited.containsKey(node)) {
			visits = _visited.get(node);
		}
		return visits.intValue();
	}

	private void incrementVisitCount(ITreeNodeModel<?> node) {
		_visited.put(node, visitCount(node) + 1);
	}

	private void preTraversal(ITreeNodeModel<?> node, ElementType type) {
		switch (type) {

		case ROOT_FIRST_VISIT:
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_DIV));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_LIST));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			break;

		case BRANCH_FIRST_VISIT:
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_ITEM));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_DIV));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, RENDER_BODY));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_DIV));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_DIV));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_LIST));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			break;

		case LEAF:
			_commands.add(new TreeRendererCommandNodeSet(node, OPEN_ITEM));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, RENDER_BODY));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			break;

		}
	}

	private void postTraversal(ITreeNodeModel<?> node, ElementType type) {
		switch (type) {

		case ROOT_LAST_VISIT:
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_LIST));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_DIV));
			break;

		case BRANCH_LAST_VISIT:
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_LIST));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_DIV));
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_ITEM));
			break;

		case LEAF:
			_commands.add(new TreeRendererCommandNodeSet(node, DO_NOTHING));
			_commands.add(new TreeRendererCommandNodeSet(node, CLOSE_ITEM));
			break;

		}
	}

}
